Skip to content

Count-Min: serialization version 2 (cross-language format) - #798

Open
leerho wants to merge 4 commits into
mainfrom
count-min-v2
Open

leerho wants to merge 4 commits into
mainfrom
count-min-v2

Conversation

@leerho

@leerho leerho commented Nov 11, 2026

Copy link
Copy Markdown
Member

This implements the cross-language Count-Min serialization format, version 2, specified in apache/datasketches-tck#21 (docs/count-min-format.md) and agreed in apache/datasketches-tck#18.

Please don't merge this until apache/datasketches-tck#21 has merged, because the class javadoc links to the spec there. It's open now so the other implementations can compare against it.

Version 2

  • Row seeds: rowSeed[i] = XXH64(LE64(i), seed).
  • Item hashing: items are hashed with XxHash64.
    • 64-bit integers are hashed as 8 little-endian bytes, strings as UTF-8, and byte arrays as given.
  • Bucket: (hash >>> 1) % numBuckets.
  • Serial version: byte 2. The byte layout is unchanged.
  • Version 1 images: they still load. Such a sketch keeps the version 1 hashing (seeds from Random, MurmurHash3, floorMod) and is written back as version 1.
  • Merging: version 1 with version 2 throws.
  • Other serial versions: rejected.
  • Null items: a null byte array is ignored like an empty one, as the spec requires. Before, it threw a NullPointerException.

Public constructors

Until now, CountMinSketch had no public way to create a sketch; only deserialize was public. This adds:

  • CountMinSketch(byte numHashes, int numBuckets), with the default seed 9001;
  • CountMinSketch(byte numHashes, int numBuckets, long seed).

Both take the values returned by suggestNumHashes and suggestNumBuckets directly.

Tests

  • CountMinSketchTest checks every test vector in section 7 of the spec:

    • XXH64 values and row seeds;
    • item-to-bucket mappings, including a non-ASCII string and hashes with the top bit set;
    • both images, byte for byte.

    It also checks:

    • that the long overloads hash the item's little-endian bytes;
    • version 1 behavior, against a separate implementation of the legacy hashing;
    • merging across versions, unknown versions, null and empty items, and the public constructors.
  • CountMinSketchCrossLanguageTest generates and checks the version 2 CL files from section 8 of the spec:

    • names are count_min_v2_<case>_<language>.sk;
    • the checker covers Java, C++, Go and Rust files;
    • a file from another language must also be byte-identical to Java's;
    • the existing C++ version 1 files are checked to load as version 1 sketches.

The full suite passes (2,393 tests).

🤖 Generated with Claude Code

leerho and others added 2 commits November 10, 2026 16:45
New sketches use the cross-language Count-Min format, version 2, defined
in the TCK (docs/count-min-format.md):
- row seeds: rowSeed[i] = XXH64(LE64(i), seed)
- items hashed with XxHash64: longs as 8 little-endian bytes, strings as
  UTF-8, byte arrays as given
- bucket = (hash >>> 1) % numBuckets
- serial version byte 2; the byte layout is unchanged

A sketch read from a version 1 image keeps the version 1 hashing
(Random seeds, MurmurHash3, floorMod) and is written back as version 1.
Merging version 1 with version 2 throws, and other serial versions are
rejected. A null byte array is now ignored, like an empty one.

Tests check every test vector in the spec (XXH64, row seeds, buckets and
both images), and the version 1 path against an independent computation
of the legacy hashing.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
- CountMinSketch had no public way to create a sketch; only deserialize
  was public. Add public constructors (numHashes, numBuckets) with the
  default seed 9001, and (numHashes, numBuckets, seed).
- Add CountMinSketchCrossLanguageTest, which generates and checks the
  version 2 CL files defined in section 8 of the TCK spec:
  count_min_v2_<case>_<language>.sk, for cases n0, n1, n10, n100, n1000,
  n10000 and seed42_n1000. Files from another language must also be
  byte-identical to Java's. The C++ version 1 files are checked to load as
  version 1 sketches.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Each case from section 8.1 of the spec now states its name, seed and N
directly, so the test no longer parses N out of the case name. This also
clears CodeQL alert 995 (possible uncaught NumberFormatException).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@leerho
leerho requested review from proost and tisonkun November 11, 2026 01:24
* The product of numHashes and numBuckets must be less than 2<sup>30</sup>.
* @throws SketchesArgumentException if a parameter is out of range
*/
public CountMinSketch(final byte numHashes, final int numBuckets) {

@proost proost Nov 11, 2026 •

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

int type can be more readable to user than byte type for numHashes. what is your intention?

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Good point, thanks. Both public constructors now take int numHashes, checked to be from 1 to 127. It is cast to a byte only where it is stored, since the image keeps it in one byte. So new CountMinSketch(3, 1000) works without a cast, and a byte from suggestNumHashes() still passes directly. getNumHashes_() and the serialization format are unchanged. A new test covers the range (0, -1, 128 and 259 are rejected; 127 round-trips).

* @param seed The base hash seed
* @throws SketchesArgumentException if a parameter is out of range
*/
public CountMinSketch(final byte numHashes, final int numBuckets, final long seed) {

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

here too.

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Changed here too. See the reply on the first constructor above.

numHashes is checked to be from 1 to 127 and cast to a byte only where
it is stored, so callers no longer need to write (byte) 3. A byte from
suggestNumHashes() still passes directly. getNumHashes_(), the image
format and deserialization are unchanged.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

This branch has not been deployed

No deployments
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants